Crosstech Solutions Group

Публикационная активность компании на Хабре

Автор

Назаровская Вероника

Дата публикации

19 декабря 2025 г.

О компании

Crosstech Solutions Group (далее – CTSG/КТСГ) – российский разработчик инновационных решений для комплексной защиты информации бизнеса. Компания предлагает готовые решения, осуществляет заказную разработку и предоставляет ИТ-услуги в области кибербезопасности. Представлена на рынке ИБ с 2018 года.

Продуктовый портфель КТСГ на сегодняшний день насчитывает 11 продуктов. Все они разработаны на основе глубоких исследований рынка информационной безопасности и закрепили за собой статус высокоэффективных средств защиты информации, пройдя апробацию в крупнейших компаниях различных отраслей экономики России. ИБ-продукты включены в реестр отечественного ПО Минцифры России и рекомендованы для импортозамещения на российских предприятиях.

Компания активно развивается не только со стороны разработки и поставки ПО, но и со стороны PR и маркетинга. Так, компания ведет соц. сети для повышения узнаваемости продуктов и развития бренда:

Цель исследования

В рамках учебной работы представлен анализ публикационной активности компании на Хабре. Я применила количественные методы, чтобы решить следующие задачи:

  • определить самые популярные статьи компании с учетом нескольких показателей;
  • выявить тематические направления, вызывающие наибольший интерес у аудитории;
  • визуализировать полученные результаты.

Я получила большое удовольствое в процессе работы. Надеюсь, тебе понравятся ее результаты ;)

Поехали!

Веб-скрапинг

На начальном этапе я скрапила все опубликованные статьи компании на Хабре. В основном работала с пакетом rvest, но в одном случае понадобилось открыть сессию через chromote. Но об этом позже.

Сначала привязала все библиотеки.

library(rvest)
library(tidyverse)
library(polite)
library(xml2)
library(chromote)
library(lubridate)

Собрала названия всех статей и ссылки на них. Привела это к тайди-формату и подготовила первую таблицу.

url <- "https://habr.com/ru/companies/ctsg/articles/"

# создаю "виртуальный браузер" для легитимизации действий парсера
session <- bow(url)
html <- scrape(session)

# сохраняю себе названия статей, класс определила через SelectorGadget
elements <- html |> 
  html_elements(".tm-title__link")

# сохраняю таблицу, в которую складываю названия статей и ссылки на них
# все это хранится в теге "a"
articles <- tibble(
  title = elements |>
    html_text2(),
  href = elements |> 
    html_attr("href")
)

# добавляю протокол и домен к "половинчатым" сслыкам
articles <- articles |> 
  mutate(link = str_c("https://habr.com", href)) |> 
  select(-href)

# сохраняю вектор ссылок на статьи
urls <- articles |> 
  pull(link)

По итогу получилась такая табличка.

title link
Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение https://habr.com/ru/companies/ctsg/articles/972514/
PAM в информационной безопасности: ценный актив или бесполезный сотрудник? https://habr.com/ru/companies/ctsg/articles/918904/
Хакатоны только для гениев? Разбираем самые популярные заблуждения https://habr.com/ru/companies/ctsg/articles/893028/
От идеи до первого выпуска: как и зачем мы запустили подкаст про ИБ? https://habr.com/ru/companies/ctsg/articles/882176/
Кто такие DevSecOps -инженеры и зачем они нужны? https://habr.com/ru/companies/ctsg/articles/867704/
Как с нуля построить систему обработки событий https://habr.com/ru/companies/ctsg/articles/842186/
Как организовать внутренний митап, чтобы он зашел команде? Наши принципы и немного истории https://habr.com/ru/companies/ctsg/articles/831464/
Хочу стать тимлидом: как выбрать свой путь от специалиста в руководители https://habr.com/ru/companies/ctsg/articles/819323/
Секреты успешного собеседования: как получить оффер технарю https://habr.com/ru/companies/ctsg/articles/818243/
Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум https://habr.com/ru/companies/ctsg/articles/808311/
Распознавание лиц на микрокомпьютерах https://habr.com/ru/companies/ctsg/articles/807069/
Эффективные вложения в ИТ: Как посчитать ROI при внедрении ПО на примере системы маскирования данных https://habr.com/ru/companies/ctsg/articles/805255/
Как выжить на первом испытательном сроке в IT и не только https://habr.com/ru/companies/ctsg/articles/803979/

Затем написала функцию, которая пройдется по всей таблице с ссылками на статьи и соберет нужные мне для анализа данные. В данном случае функция будет “вытаскивать”:

  • тексты статей;
  • дата;
  • время публикации;
  • время чтения;
  • количество голосов (грубо говоря, лайков);
  • количество добавлений в избранное;
  • число комментариев;
  • теги, хабы;
  • уровень сложности текста.
get_article_polite <- function(url) {
  bow_obj <- bow(url)
  
  html_page <- scrape(bow_obj)
  
  months_dict <- c(
    "янв" = "01",
    "фев" = "02",
    "мар" = "03",
    "апр" = "04",
    "мая" = "05",
    "июн" = "06",
    "июл" = "07",
    "авг" = "08",
    "сен" = "09",
    "окт" = "10",
    "ноя" = "11",
    "дек" = "12"
  )
  
  date = html_page |> 
    html_elements(".tm-article-presenter__snippet time") |> 
    html_text2() |> 
    str_replace(" в ", " ") |>
    str_replace("\\d+:\\d+", "")
  
  if (!str_detect(date, "202")) {date <- paste(date, year(Sys.Date()))}
  
  res = tibble(
    text = html_page |>
      html_elements(".article-body") |> 
      html_text2() |> 
      str_squish() |>
      str_remove("^\\[\\] "),
    date = date |> 
      str_squish() |> 
      str_replace_all(months_dict) |> 
      dmy(),
    time = html_page |> 
      html_elements(".tm-article-presenter__snippet time") |> 
      html_text2() |> 
      str_extract("\\d+:\\d+") |> 
      hm(),
    read_time_min = html_page |> 
      html_elements(".tm-article-presenter__snippet .tm-article-reading-time__label") |> 
      html_text2() |> 
      str_extract("\\d+") |> 
      as.integer(),
    votes = html_page |> 
      html_element(".tm-votes-lever__score_appearance-article span, .votes-switcher") |> 
      html_text2() |> 
      str_extract("(?<=\\+).") |>  # без этой регулярки отображается доп. инфа, необходимо взять только последнее число
      as.integer(),
    mark = html_page |> 
      html_elements(".tm-article-sticky-panel__icons .bookmarks-button") |> 
      html_text2() |> 
      str_remove("Добавить в закладки") |> 
      as.integer(), 
    num_of_comments = html_page |> 
      html_elements(".tm-article-sticky-panel__icons .article-comments-counter-link-wrapper") |> 
      html_text2() |> 
      str_remove("Комментарии") |> 
      as.integer(),
    tags = html_page |> 
      html_elements(".tag-list") |> 
      html_text2() |> 
      str_extract_all("(?<=\\[).*?(?=\\])") |> 
      unlist() |>
      paste(collapse = ", "),
    hubs = html_page |> 
      html_elements(".tm-article-presenter__meta-list+ .tm-article-presenter__meta-list") |> 
      html_text2() |> 
      str_extract_all("(?<=\\[).*?(?=\\])") |> 
      unlist() |>
      paste(collapse = ", "),
    complexity_label = html_page |> 
      html_element(".tm-article-complexity_complexity-medium .tm-article-complexity__label") |> 
      html_text2()
  )
  
  return(res)
  
}

Применяю функцию ко всем статьям через итератор

articles_ctsg <- map_df(urls, get_article_polite, .id = "id")

И в результате получаем следующее. В таблице ниже в некоторых ячейках обрезаны строки, чтобы визуальнее это смотрелось приятнее. На итоговый тиббл я оставлю ссылку для скачивания.

id text date time read_time_min votes mark num_of_comments tags hubs complexity_label
1 Всем привет! На связи Александр Синичкин, ведущий архитектор … 2025-12-02 13H 44M 0S 9 8 9 0 контейнеризация, контейнерная безопасность, разработка продукта, кейс, container security, … Блог компании Crosstech Solutions Group, Kubernetes, IT-инфраструктура, IT-компании Средний
2 PAM или партнерский менеджер — специалист, отвечающий за … 2025-06-17 7H 51M 0S 7 5 2 4 продажи в it, информационная безопасность, партнеры, партнерские отношения, … Блог компании Crosstech Solutions Group, Информационная безопасность NA
3 Хакатон — это марафон в мире IT. Здесь … 2025-03-21 9H 30M 0S 3 6 7 1 хакатон, командообразование, защита проекта, тестировщик, студенты it, студенты Блог компании Crosstech Solutions Group NA
4 Привет! Это Яна Ильина, HRBP CrossTech Solutions Group, … 2025-02-13 11H 29M 0S 6 6 9 2 подкаст, бренд, выпуски, команда, информационная безопасность, спикеры Блог компании Crosstech Solutions Group, Информационная безопасность NA
5 Добрый день, уважаемые читатели! Сегодня я расскажу о … 2024-12-18 12H 30M 0S 5 NA 13 3 DevSecOps -инженеры, информационная безопасность, тестирование, уязвимости Блог компании Crosstech Solutions Group NA
6 Сегодня Александр Шувалов и Юлиян Латыпов поделятся с … 2024-09-10 10H 24M 0S 7 3 28 0 данные, потоковая обработка, потоковая обработка данных Блог компании Crosstech Solutions Group, Анализ и проектирование … NA
7 Всем привет! Меня зовут Ульяна Петракова, я специалист … 2024-07-25 13H 8M 0S 2 4 8 1 it-компании, карьера в it-индустрии, митап Блог компании Crosstech Solutions Group, Управление персоналом NA
8 Когда я работал программистом, мне было интересно не … 2024-06-04 7H 50M 0S 17 1 40 0 управление, тимлидство, путь в ит, развитие, менеджмент, лидерство Блог компании Crosstech Solutions Group, Управление разработкой, Управление … NA
9 Привет! Меня зовут Артём и когда-то я уже … 2024-05-30 8H 37M 0S 11 7 44 8 собеседование в it, подбор персонала, интервью, рекрутинг, интервью … Блог компании Crosstech Solutions Group, Карьера в IT-индустрии, … NA
10 Всем привет! С вами снова я, Артём Харченков, … 2024-04-17 7H 48M 0S 12 3 20 9 карьера в it-индустрии, it компании, информационная безопасность Блог компании Crosstech Solutions Group NA
11 В последние годы появляется всё больше технологий с … 2024-04-11 14H 8M 0S 9 9 52 7 информационная безопасность, распознавание лиц Блог компании Crosstech Solutions Group, Машинное обучение NA
12 Всем привет! Меня зовут Али Гаджиев, я Директор … 2024-04-04 6H 28M 0S 7 4 9 2 защита данных, защита от утечек данных, субд Блог компании Crosstech Solutions Group, Хранение данных Средний
13 Всем привет! Меня зовут Артём Харченков, и я … 2024-03-29 14H 36M 0S 13 3 105 16 испытательный срок, информационная безопасность, подбор персонала Блог компании Crosstech Solutions Group, Информационная безопасность, Карьера … NA

Далее делаю кое-какие преобразования, чтобы можно было собрать отдельную статистику по дням и по времени.

months_dict2 <- c(
  "янв" = "Jan",
  "фев" = "Feb",
  "мар" = "Mar",
  "апр" = "Apr",
  "май" = "May",
  "июн" = "Jun",
  "июл" = "Jul",
  "авг" = "Aug",
  "сен" = "Sep",
  "окт" = "Oct",
  "ноя" = "Nov",
  "дек" = "Dec"
)

week_dict <- c(
  "Пн" = "Mon",
  "Вт" = "Tue",
  "Ср" = "Wed",
  "Чт" = "Thu",
  "Пт" = "Fri",
  "Сб" = "Sat",
  "Вс" = "Sun"
)

articles_ctsg <- articles_ctsg |> 
  mutate(year = year(date), 
         month = month(date, label = TRUE),
         wday = wday(date, label = TRUE, locale = Sys.getlocale("LC_TIME")),
         hour = hour(time),
         length = str_count(text, "\\S+")) |> 
  mutate(month = str_replace_all(as.character(month), months_dict2), 
         wday = str_replace_all(as.character(wday), week_dict))

В Хабре не так давно прошло обновление. Теперь он показывает число уникальных пользователей, которые:

  • Открыли публикацию;
  • Открыли публикацию ИЛИ увидели еe в ленте.

Значит, второй показатель будет заведомо больше. И он позволит HR и PR-менеджерам делать дополнительные выводы в ходе анализа интересов аудитории.

Нажмите на картинку, чтобы ее приблизить.

Но сам этот элемент является popup-элементом. Т.е. он открывается только при нажатии не него. Через SelectorGadget у меня не получилось до него достучаться, поэтому я использовала пакет chromote.

И затем я извлекаю количество просмотров.

get_article_2 <- function(url) {
  b <- ChromoteSession$new()
  
  b$Network$enable()
  b$Network$setBlockedURLs(urls = list(
    "*googletagmanager.com/*",
    "*google-analytics.com/*",
    "*analytics.google.com/*",
    "*mc.yandex.ru/*",
    "*yandex.ru/ads/*",
    "*vk.com/rtrg*",
    "*sentry*"
  ))
  b$Page$navigate(url)
  b$Page$loadEventFired(wait_ = TRUE)
  Sys.sleep(3)
  
  CLICK_SELECTOR <- ".tm-article-presenter__snippet .reach-counter"
  
  js_click <- sprintf("
  (function(){
    var el = document.querySelector('%s');
    if (el) { el.click(); return 'clicked'; }
    else { return 'not found'; }
  })();
", CLICK_SELECTOR)
  
  res <- b$Runtime$evaluate(js_click)
  
  Sys.sleep(2)
  
  html <- {
    doc <- b$DOM$getDocument()
    root_id <- doc$root$nodeId
    b$DOM$getOuterHTML(nodeId = root_id)[['outerHTML']]
  }
  
  page <- read_html(html)
  
  res = tibble(
    reach_value = page |>
      html_element(".tm-modal-window .value") |>
      html_text2() |> 
      str_remove(" охват"),
    unique_readers = page |> 
      html_element(".publication-metric+ .publication-metric .value") |>
      html_text2() |> 
      str_remove(" читател(ей|я|ь)")
  )
  
  return(res)
  
}

articles_ctsg_2 <- map_df(urls, get_article_2, .id = "id")

В хабре количество просмотров, превыщающее 1 тысячу, обозначается через К, например, 1.2к - 1200 просмотров. Ниже будет представлена функция, которая будет извлекать приблизительное число просмотров и переводить его в числовой формат. Да, значение получится округленным, но это зато примет более нормализованный вид.

change_views <- function(vec) {
  map_int(vec, ~ {
    x <- .x
    # если запись содержит символ точки или "К"
    if (is.na(as.integer(x)) == "TRUE") {
      # сначала удаляю "K"
      x <- str_remove(x, "K")
      # если после удаления "К" есть еще один ненужный элемент - точка
      if (str_detect(x, "\\.")) {
        # то удаляем точку
        x <- str_remove(x, "\\.")
        # и приписываем два нуля (условно умножаем на тысячу)
        x <- str_c(x, "00")
        # переводим к целочисленному типу
        x <- as.integer(x)
      } 
      else {
        # если точки нет, то просто приписываем 3 нуля (будто умножаем на тысячу)
        # и переводим к целому типу
        x <- str_c(x, "000")
        x <- as.integer(x)
      }
    }
    # если нет ни точки, ни "К", то просто переводим к целому типу данных
    else {
      x <- as.integer(x)
    }
  })
}

Применяю функцию к полученному тибблу.

articles_ctsg_2 <- articles_ctsg_2 |> 
  mutate(reach_value = change_views(reach_value)) |> 
  mutate(unique_readers = change_views(unique_readers)) |> 
  mutate(id = as.integer(id))

И получается такая милая табличка.

id reach_value unique_readers
1 7300 677
2 478 478
3 751 751
4 703 703
5 2100 2100
6 2200 2200
7 743 743
8 2100 2100
9 3800 3800
10 4000 4000
11 5400 5400
12 2900 2900
13 23000 23000

Обработка данных

После того как все данные собраны, я делаю дополнительные преобзования. Во-первых, разделяю колонки с датами на отдельные колонки, добавляю колонку с количеством слов в тексте, и заменяю значения месяцев и дней недели по словарям.

months_dict2 <- c(
  "янв" = "Jan",
  "фев" = "Feb",
  "мар" = "Mar",
  "апр" = "Apr",
  "май" = "May",
  "июн" = "Jun",
  "июл" = "Jul",
  "авг" = "Aug",
  "сен" = "Sep",
  "окт" = "Oct",
  "ноя" = "Nov",
  "дек" = "Dec"
)

week_dict <- c(
  "Пн" = "Mon",
  "Вт" = "Tue",
  "Ср" = "Wed",
  "Чт" = "Thu",
  "Пт" = "Fri",
  "Сб" = "Sat",
  "Вс" = "Sun"
)

articles_ctsg <- articles_ctsg |> 
  mutate(year = year(date), 
         month = month(date, label = TRUE),
         wday = wday(date, label = TRUE, locale = Sys.getlocale("LC_TIME")),
         hour = hour(time),
         length = str_count(text, "\\S+")) |> 
  mutate(month = str_replace_all(as.character(month), months_dict2), 
         wday = str_replace_all(as.character(wday), week_dict))

Во-вторых, добавляю отдельную колонку с ID статьи. Также преобразовываю общую колонку с временем и отдельную колонку с часами: прибавляю +3 часа, чтобы отображаемое время публикации статьи было московским.

articles_ctsg <- articles_ctsg |> 
  mutate(id = as.integer(id),
         time = time + hours(3),
         hour = hour + 3)

# добавляю к исходному тибблу с названиями статей и ссылками на них идентификатор
articles <- articles |> 
  mutate(id = row_number())

В-третьих, делаю два иннер-джойна (ведь таблиц у меня 3, за раз можно объединить только 2).

articles_ctsg <- articles_ctsg |> 
  inner_join(articles_ctsg_2, by = "id")

# объединяю две таблицы через inner join (т.к. здесь нет пропусков)
articles_ctsg_full <- inner_join(articles, articles_ctsg, by = "id") |> 
  select(id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs)

И у меня получается общая таблица со всеми собранными данными. Целая таблица для скачивания представлена по ссылке.

Обращаю внимание, что при воспроизведении кода результаты могут немного изменяться. Это связано с тем, что я локально прогнала этот код и подгрузила его в проект, чтобы облегчить работу рендеру. Поэтому у Вас могут быть другие показатели как минимум просмотров.

id title text length link complexity_label date year month wday hour time read_time_min reach_value unique_readers votes mark num_of_comments tags hubs
1 Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение Всем привет! На связи Александр Синичкин, ведущий архитектор … 1851 https://habr.com/ru/... Средний 2025-12-02 2025 Dec Tue 16 16H 44M 0S 9 7300 677 8 9 0 контейнеризация, контейнерная безопасность, … Блог компании Crosstech …
2 PAM в информационной безопасности: ценный актив или бесполезный сотрудник? PAM или партнерский менеджер — специалист, отвечающий за … 1420 https://habr.com/ru/... NA 2025-06-17 2025 Jun Tue 10 10H 51M 0S 7 478 478 5 2 4 продажи в it, … Блог компании Crosstech …
3 Хакатоны только для гениев? Разбираем самые популярные заблуждения Хакатон — это марафон в мире IT. Здесь … 652 https://habr.com/ru/... NA 2025-03-21 2025 Mar Fri 12 12H 30M 0S 3 751 751 6 7 1 хакатон, командообразование, защита … Блог компании Crosstech …
4 От идеи до первого выпуска: как и зачем мы запустили подкаст про ИБ? Привет! Это Яна Ильина, HRBP CrossTech Solutions Group, … 1172 https://habr.com/ru/... NA 2025-02-13 2025 Feb Thu 14 14H 29M 0S 6 703 703 6 9 2 подкаст, бренд, выпуски, … Блог компании Crosstech …
5 Кто такие DevSecOps -инженеры и зачем они нужны? Добрый день, уважаемые читатели! Сегодня я расскажу о … 1033 https://habr.com/ru/... NA 2024-12-18 2024 Dec Wed 15 15H 30M 0S 5 2100 2100 NA 13 3 DevSecOps -инженеры, информационная … Блог компании Crosstech …
6 Как с нуля построить систему обработки событий Сегодня Александр Шувалов и Юлиян Латыпов поделятся с … 1354 https://habr.com/ru/... NA 2024-09-10 2024 Sep Tue 13 13H 24M 0S 7 2200 2200 3 28 0 данные, потоковая обработка, … Блог компании Crosstech …
7 Как организовать внутренний митап, чтобы он зашел команде? Наши принципы и немного истории Всем привет! Меня зовут Ульяна Петракова, я специалист … 367 https://habr.com/ru/... NA 2024-07-25 2024 Jul Thu 16 16H 8M 0S 2 743 743 4 8 1 it-компании, карьера в … Блог компании Crosstech …
8 Хочу стать тимлидом: как выбрать свой путь от специалиста в руководители Когда я работал программистом, мне было интересно не … 4323 https://habr.com/ru/... NA 2024-06-04 2024 Jun Tue 10 10H 50M 0S 17 2100 2100 1 40 0 управление, тимлидство, путь … Блог компании Crosstech …
9 Секреты успешного собеседования: как получить оффер технарю Привет! Меня зовут Артём и когда-то я уже … 2766 https://habr.com/ru/... NA 2024-05-30 2024 May Thu 11 11H 37M 0S 11 3800 3800 7 44 8 собеседование в it, … Блог компании Crosstech …
10 Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум Всем привет! С вами снова я, Артём Харченков, … 3032 https://habr.com/ru/... NA 2024-04-17 2024 Apr Wed 10 10H 48M 0S 12 4000 4000 3 20 9 карьера в it-индустрии, … Блог компании Crosstech …
11 Распознавание лиц на микрокомпьютерах В последние годы появляется всё больше технологий с … 2022 https://habr.com/ru/... NA 2024-04-11 2024 Apr Thu 17 17H 8M 0S 9 5400 5400 9 52 7 информационная безопасность, распознавание … Блог компании Crosstech …
12 Эффективные вложения в ИТ: Как посчитать ROI при внедрении ПО на примере системы маскирования данных Всем привет! Меня зовут Али Гаджиев, я Директор … 1633 https://habr.com/ru/... Средний 2024-04-04 2024 Apr Thu 9 9H 28M 0S 7 2900 2900 4 9 2 защита данных, защита … Блог компании Crosstech …
13 Как выжить на первом испытательном сроке в IT и не только Всем привет! Меня зовут Артём Харченков, и я … 3348 https://habr.com/ru/... NA 2024-03-29 2024 Mar Fri 17 17H 36M 0S 13 23000 23000 3 105 16 испытательный срок, информационная … Блог компании Crosstech …

Анализ данных

Теперь мне было интересно узнать, какие статьи компании оказались самыми популярными. Выводы необходимо делать на основании тех показателей, которые были получены во время сбора данных.

Так, я решила посмотреть ТОП-5 статей по следующим данным:

  • Просмотры (по первому и второму показателю просмотров Хабра);
  • Число голосов;
  • Число добавлений статей в Избранные;
  • Число комментариев.
most_viewed1 <- articles_ctsg_full |> 
  arrange(desc(unique_readers)) |> 
  head(5)

most_viewed2 <- articles_ctsg_full |> 
  arrange(desc(reach_value)) |> 
  head(5)

most_votes <- articles_ctsg_full |> 
  arrange(desc(votes)) |> 
  head(5)

most_marked <- articles_ctsg_full |> 
  arrange(desc(mark))|> 
  head(5)

most_commented <- articles_ctsg_full |> 
  arrange(desc(num_of_comments))|> 
  head(5)

Сохраняю ID тех статей, которые являются наиболее значимыми с разных точек зрения (читатели, охват, оценки, комментарии), и формирует итоговый список “ключевых” публикаций компании. После формирую общую таблицу.

all_ids <- unique(c(
  most_viewed1$id,
  most_viewed2$id,
  most_votes$id,
  most_marked$id,
  most_commented$id
))

result <- tibble(
  id = all_ids,
  in_most_viewed1 = id %in% most_viewed1$id,
  in_most_viewed2 = id %in% most_viewed2$id,
  in_most_votes = id %in% most_votes$id,
  in_most_marked = id %in% most_marked$id,
  in_most_commented = id %in% most_commented$id
)

Здесь мы видим, какие статьи входят во все топы, но еще не хватает некоторой структуры.

library(knitr)
library(kableExtra)
library(gt)

result |>
  gt() |>
  opt_table_outline() |>
  
  tab_options(
    table.width = px(500)
  ) |>

  # Цвет текста (чуть темнее базового spacelab)
  tab_style(
    style = cell_text(color = "#2f3a45"),
    locations = cells_body()
  ) |>

  # Шапка таблицы
  tab_style(
    style = list(
      cell_fill(color = "#eef5ff"),
      cell_text(weight = "bold", color = "#2f3a45")
    ),
    locations = cells_column_labels()
  ) |>

  # Hover-эффект для строк
  opt_css(
    css = "
      tbody tr:hover {
        background-color: #f7fbff;
      }
    "
)
id in_most_viewed1 in_most_viewed2 in_most_votes in_most_marked in_most_commented
13 TRUE TRUE FALSE TRUE TRUE
11 TRUE TRUE TRUE TRUE TRUE
10 TRUE TRUE FALSE FALSE TRUE
9 TRUE TRUE TRUE TRUE TRUE
12 TRUE FALSE FALSE FALSE FALSE
1 FALSE TRUE TRUE FALSE FALSE
3 FALSE FALSE TRUE FALSE FALSE
4 FALSE FALSE TRUE FALSE FALSE
8 FALSE FALSE FALSE TRUE FALSE
6 FALSE FALSE FALSE TRUE FALSE
2 FALSE FALSE FALSE FALSE TRUE

Поэтому надо просуммировать все строчки и отсортировать по общей сумме. И соединить эту таблицу с последней, чтобы она содержала все данные по каждой “топовой” статье.

result <- result |> 
  mutate(total_hits = rowSums(across(starts_with("in_")))) |> 
  arrange(desc(total_hits)) |> 
  inner_join(articles_ctsg_full, by = "id") |> 
  select(total_hits, id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs) |>
  head(5) |> 
  mutate(hit_id = row_number()) |> 
  mutate(hit_id = as.integer(hit_id))

result <- result |> 
  select(hit_id, total_hits, id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs)

Получилась такая таблица. Забрать целую таблицу можно по ссылке. Также дисклеймер, что данные при воспроизведении могут отличаться от представленных.

hit_id total_hits id title text length link complexity_label date year month wday hour time read_time_min reach_value unique_readers votes mark num_of_comments tags hubs
1 5 11 Распознавание лиц на … В последние годы появляется … 2022 https://habr.com/ru/... NA 2024-04-11 2024 Apr Thu 17 17H 8M 0S 9 5400 5400 9 52 7 информационная безопасность, … Блог компании …
2 5 9 Секреты успешного собеседования: … Привет! Меня зовут Артём … 2766 https://habr.com/ru/... NA 2024-05-30 2024 May Thu 11 11H 37M 0S 11 3800 3800 7 44 8 собеседование в … Блог компании …
3 4 13 Как выжить на … Всем привет! Меня зовут … 3348 https://habr.com/ru/... NA 2024-03-29 2024 Mar Fri 17 17H 36M 0S 13 23000 23000 3 105 16 испытательный срок, … Блог компании …
4 3 10 Как тимлиду проводить … Всем привет! С вами … 3032 https://habr.com/ru/... NA 2024-04-17 2024 Apr Wed 10 10H 48M 0S 12 4000 4000 3 20 9 карьера в … Блог компании …
5 2 1 Как создать решение … Всем привет! На связи … 1851 https://habr.com/ru/... Средний 2025-12-02 2025 Dec Tue 16 16H 44M 0S 9 7300 677 8 9 0 контейнеризация, контейнерная … Блог компании …

Итого, в общий ТОП-5 вошли следующие статьи:

  • Распознавание лиц на микрокомпьютерах1;
  • Секреты успешного собеседования: как получить оффер технарю2;
  • Как выжить на первом испытательном сроке в IT и не только3;
  • Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум4;
  • Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение5.

Чуть позже я попытаюсь сделать выводы о том, почему именно такие статьи оказались наиболоее востребованными у аудитории, предварительно рассмотрев некоторые лексические особенности этих текстов. А пока предлагаю ознакомиться с незамысловатой визуаилизацией, которую я подготовила по обработанным данным :)

Визуализация

Таблицы нам уже могут рассказать некоторую особенность, но визуализация не только дает красивую картинку, но и помогает выявить интересные закономерности в данных. Предлагаю ознакомиться с тем, что у меня получилось!

Сделаю замечание, что здесь представлена статистика по ВСЕМ статьям компании на Хабре, а не только по самым популярным.

Подгружаю библиотеки.

library(gridExtra)
library(grid)
library(paletteer)
library(extrafont)
library(plotly)
library(dplyr)

Здесь я хочу посмотреть число статей по месяцам. Для этого сначала посчитаю, сколько статей было написано за определенный год и месяц.

month_order <- c(
  "Jan","Feb","Mar","Apr","May","Jun",
  "Jul","Aug","Sep","Oct","Nov","Dec"
)

years_order <- c(2024, 2025)

articles_ctsg_full_p1 <- articles_ctsg_full |> 
  count(year, month, sort = FALSE) |> 
  tidyr::complete(
    year = years_order,
    month = month_order,
    fill = list(n = 0)
  ) |>
  mutate(
    month = factor(month, levels = month_order),
    year  = factor(year)
  ) |>
  rename(count = n)

В целом, это уже готово к визуализации. Только делаю некоторые махинации с палитрой и шрифтом. Предварительно скачиваю фон Montserrat, его можно забрать здесь.

font_import()  
loadfonts(device = "win")

И готовю график. Сначала через ggplot, потом “оживляю” график через plotly.

p1 <- articles_ctsg_full_p1 |> 
  mutate(month = factor(month, levels = month_order),
         year  = factor(year)) |>
  ggplot(aes(x = month,
             y = count,
             fill = year)) +
  geom_col(alpha = 0.8, na.rm = TRUE) +
  scale_fill_manual(name = "Год",
                    values = c("2024" = "#3A77B9", "2025" = "#385E8C")) +
  scale_x_discrete(labels = month_order) +
  labs(title = "Число статей по месяцам",
       x = NULL, y = NULL) +
  theme(
    panel.background = element_rect(fill = "white", color = NA),
    panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
    panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
    text = element_text(size = 14, family = "Montserrat Medium"))

ggplotly(p1) |> 
  layout(
    legend = list(
      x = 1.05,
      y = 0.5,
      xanchor = "left",
      yanchor = "middle",
      title = list(
        text = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Год"
      )
    )
  )

Апрель 2024 года был самым продуктивным годом. 2 из 5 самых популярных статей были опубликованы в этот период.

Теперь рассмотрим статьи по дням недели. Для этого в первую очередь необходимо посчитать число статей по годам и дням недели.

week_order <- c(
  "Fri", "Thu", "Wed", "Tue", "Mon" 
)

years_order <- c(2024, 2025)

articles_ctsg_full_p2 <- articles_ctsg_full |> 
  count(year, wday, sort = FALSE) |> 
  tidyr::complete(
    year = years_order,
    wday = week_order,
    fill = list(n = 0)
  ) |>
  mutate(
    year  = factor(year)
  ) |>
  rename(count = n)

Данные для визуализации готовы! Логика остается та же.

p2 <- articles_ctsg_full_p2 |>
  mutate(
    wday = factor(wday, levels = week_order),
    year = factor(year)
  ) |>
  ggplot(aes(x = wday,
             y = count,
             fill = year)) +
  geom_col(alpha = 0.8, na.rm = TRUE) +
  coord_flip() +
  scale_fill_manual(
    name = "Год",
    values = c("2024" = "#3A77B9", "2025" = "#385E8C")
  ) +
  scale_x_discrete(labels = week_order) +
  labs(
    title = "Статьи по дням недели",
    x = NULL, y = NULL
  ) +
  theme(
    panel.background = element_rect(fill = "white", color = NA),
    panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
    panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
    text = element_text(size = 14, family = "Montserrat Medium")
  )

ggplotly(p2) |> 
  layout(
    legend = list(
      x = 1.05,
      y = 0.5,
      xanchor = "left",
      yanchor = "middle",
      title = list(
        text = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Год"
      )
    )
  )

Самыми “активными” для публикаций днями являются вторник и четверг. Эти дни выбраны не просто так, об этом пишут специалисты6.

Ну и посмотрим последний (в этом разделе!) график, в котором представлены длины статей по годам.

p3 <- articles_ctsg_full |> 
  ggplot(aes(as.factor(year), length, fill = as.factor(year))) +
  geom_boxplot(show.legend = FALSE) +
  scale_fill_manual(
    name = "Год",
    values = c("2024" = "#3A77B9", "2025" = "#385E8C")
  ) +
  labs(title = "Длина статьи по годам") + 
  labs(x = NULL, y = NULL) + 
  theme(
    panel.background = element_rect(fill = "white", color = NA),
    panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
    panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
    text = element_text(size = 14, family = "Montserrat Medium")
  )

ggplotly(p3) |> 
  layout(
    legend = list(
      x = 1.05,
      y = 0.5,
      xanchor = "left",
      yanchor = "middle",
      title = list(
        text = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Год"
      )
    )
  )

Здесь мы видим, что статистически длиннее статьи были в 2024 году. Выбросы в обе стороны также ярко выражены именно в этом году. Могу предложить, что в 2025 году были введены требования оформления статей, которые регламентируют даже длину статьи.

Теперь перейдите на вкладу NLP-анализ, чтобы продолжить изучать мое исследование!

Литература

1. Распознавание лиц на микрокомпьютерах // https://habr.com/ru/companies/ctsg/articles/807069/
2. Секреты успешного собеседования: как получить оффер технарю // https://habr.com/ru/companies/ctsg/articles/818243/
3. Как выжить на первом испытательном сроке в IT и не только // https://habr.com/ru/companies/ctsg/articles/803979/
4. Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум // https://habr.com/ru/companies/ctsg/articles/808311/
5. Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение // https://habr.com/ru/companies/ctsg/articles/972514/
6. Когда лучше всего публиковать статьи в блог (Статистика из США и России) // https://habr.com/ru/companies/changeagain/articles/298490/

Сноски

  1. Распознавание лиц на микрокомпьютерах↩︎

  2. Секреты успешного собеседования: как получить оффер технарю↩︎

  3. Как выжить на первом испытательном сроке в IT и не только↩︎

  4. Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум↩︎

  5. Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение↩︎

  6. Когда лучше всего публиковать статьи в блог (Статистика из США и России)↩︎